/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel;

import static ch.msoftch.asm.Opcodes.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import sun.reflect.*;
import ch.msoftch.asm.*;
import ch.msoftch.asm.Type;
import ch.msoftch.asm.tree.*;
import ch.msoftch.internal.*;
import ch.simpleel.accessors.*;
import ch.simpleel.internal.*;

/**
 * Helper class transforming a sequence of calls to an simple EL expression.<br>
 * <code>
 * String name = PropertyResolver.path(PropertyResolver.get(Bar.class).getA());
 * </code><br>
 * This looks like this when using static imports:<br>
 * <code>
 * String name = path(get(Bar.class).getA());
 * </code><br>
 * You may also create EL paths for nested properties:<br>
 * <code>
 * String name = path(get(Bar.class).getChild().getA());
 * </code> <br>
 * Though this works only with non final child classes!
 * 
 * @author M. Hautle
 */
public final class ELStringBuilder {
    /** The class loader of our class - used to build {@link CompositeClassLoader}s. */
    private static final ClassLoader LOADER = ELStringBuilder.class.getClassLoader();

    /** Internal name of {@link ELStringBuilder}. */
    private static final String PROPERTY_RESOLVER = Type.getInternalName(ELStringBuilder.class);

    /** Internal name of {@link Serializable}. */
    private static final String SERIALIZABLE = Type.getInternalName(Serializable.class);

    /** Array holding the internal name of {@link Exception}. */
    private static final String[] THROWS_EXPECTION = new String[] { Type.getInternalName(Exception.class) };

    /** Internal name of {@link Object}. */
    private static final String OBJECT = Type.getInternalName(Object.class);

    /** Internal name of {@link ELRecorder}. */
    private static final String EL_RECORDER_TYPE = Type.getInternalName(ELRecorder.class);

    /** Cached 'recorders'. */
    private static final ICache<Class, Object> INSTANCES = CacheFactory.createCache(Class.class, Object.class);

    /** The composite loaders - used to. */
    private static final ICache<ClassLoader, CompositeClassLoader> LOADERS = CacheFactory.createCache(ClassLoader.class, CompositeClassLoader.class);

    /** Instance provider. */
    private static final InstantiationProvider PROVIDER = createProvider();

    /**
     * Returns an recording instance of the given type.
     * 
     * @param <T> The desired type
     * @param c The desired type
     * @return A recording instance of the given type
     * @throws IllegalArgumentException If an invalid type gets passed
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Class<T> c) throws IllegalArgumentException {
        final T res = (T) get0(c);
        // reset the current recorder
        ELRecorder.reset();
        return res;
    }

    /**
     * This method is public due to an implementation side-effect - don't call it yourself!<br>
     * Use {@link #get(Class)} instead.
     * 
     * @param c The desired type
     * @return A recording instance of the given type
     * @throws IllegalArgumentException If an invalid type gets passed
     */
    public static Object get0(Class<?> c) throws IllegalArgumentException {
        // we don't care about concurrency - the cache is just to avoid unnecessary object trashing
        Object o = INSTANCES.get(c);
        if (o == null)
            INSTANCES.put(c, o = PROVIDER.newInstance(getRecorderClass(c)));
        return o;
    }

    /**
     * Returns the recorded path.
     * 
     * @param o The return value (will be ignored)
     * @return The path of the calls as EL expression
     */
    public static String path(Object o) {
        final String path = ELRecorder.getPath();
        if (path.length() == 0)
            throw new IllegalStateException("No path was recorder - did you forgot to call a getter or was the getter declared as final?");
        return path;
    }

    /**
     * Returns an {@link IValueAccessor} for the recorded el path.
     * 
     * @param <T> The return type
     * @param o The return value (will be ignored)
     * @param returnType The return type of the expression or null if not known
     * @return An corresponding accessor
     */
    @SuppressWarnings("unchecked")
    public static <T> IValueAccessor<T> accessor(Object o, Class returnType) {
        return ValueAccessorManager.getELAccessor(path(o), returnType);
    }

    /**
     * Hidden constructor.
     */
    private ELStringBuilder() {
    }

    /**
     * Returns a recorder class from the given type.
     * 
     * @param <T> The type
     * @param c The desired type
     * @return The corresponding recorder class
     * @throws IllegalArgumentException If an invalid type gets passed
     */
    @SuppressWarnings("unchecked")
    private static synchronized <T> Class<? extends T> getRecorderClass(Class<T> c) throws IllegalArgumentException {
        if (c == null || c.isPrimitive() || c.isArray() || Modifier.isFinal(c.getModifiers()))
            throw new IllegalArgumentException("Only normal, non final classes were accepted!");
        final String name = buildName(c);
        final ASMClassLoader loader = getLoader(c);
        Class cl = loader.getLoadedClass(name);
        if (cl == null)
            cl = loader.defineClass(name, build(name, c));
        return cl;
    }

    /**
     * Returns the {@link ASMClassLoader} to use for the given class.<br>
     * The parent loader passed to the {@link ASMClassLoader} is a {@link CompositeClassLoader} built by the loader owning the given class and our loader (
     * {@link #LOADER}). This is necessary to ensure that every created class sees it's parent class and our {@link ELRecorder}.
     * 
     * @param <T> The class type
     * @param c The class
     * @return The responsible {@link ASMClassLoader}
     */
    private static <T> ASMClassLoader getLoader(Class<T> c) {
        ClassLoader l = c.getClassLoader();
        // handle system/bootstrap loader
        if (l == null)
            l = ClassLoader.getSystemClassLoader();
        CompositeClassLoader comp = LOADERS.get(l);
        if (comp == null)
            LOADERS.put(l, comp = new CompositeClassLoader(l, LOADER));
        return ASMClassLoader.getLoader(comp);
    }

    /**
     * Builds the name for the recorder of the given class.
     * 
     * @param c The class
     * @return The name for the recorder
     */
    private static String buildName(Class<?> c) {
        final String name = c.getName();
        if (name.startsWith("java."))
            return "my" + name + "$res";
        return name.concat("$res");
    }

    /**
     * Builds the recorder class.
     * 
     * @param name The name of the new class
     * @param c The parent class
     * @return The bytecode
     */
    @SuppressWarnings("unchecked")
    private static byte[] build(String name, Class<?> c) {
        try {
            final boolean interfaze = c.isInterface();
            final ClassNode cn = new ClassNode();
            cn.name = name.replace('.', '/');
            cn.access = ACC_PUBLIC | ACC_FINAL;
            cn.version = V1_5;
            if (interfaze) {
                cn.superName = OBJECT;
                cn.interfaces.add(Type.getInternalName(c));
            } else {
                cn.superName = Type.getInternalName(c);
            }
            createDefaultConstructor(cn);
            final List methods = cn.methods;
            for (Method read : c.getMethods()) {
                // skip synthetic, static and final methods
                if (read.isSynthetic() || ((Modifier.STATIC | Modifier.FINAL) & read.getModifiers()) != 0)
                    continue;
                // skip methods with arguments or void return value
                if (read.getParameterTypes().length > 0 || void.class.equals(read.getReturnType()))
                    continue;
                final String n = read.getName();
                final int len = n.length();
                if (n.startsWith("get") && len > 3 || n.startsWith("is") && len > 2 && boolean.class == read.getReturnType())
                    methods.add(addProperty(read));
            }
            PROVIDER.prepareForInstantiation(cn);
            final ClassWriter w = new ClassWriter(0);
            cn.accept(w);
            return w.toByteArray();
        } catch (SecurityException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Adds a default constructor.
     * 
     * @param cn The classnode
     */
    @SuppressWarnings("unchecked")
    private static void createDefaultConstructor(ClassNode cn) {
        final MethodNode c = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, THROWS_EXPECTION);
        final InsnList inst = c.instructions;
        inst.add(new VarInsnNode(ALOAD, 0));
        inst.add(new MethodInsnNode(INVOKESPECIAL, cn.superName, "<init>", "()V"));
        inst.add(new InsnNode(RETURN));
        c.maxLocals = 1;
        c.maxStack = 1;
        cn.methods.add(c);
    }

    /**
     * Adds a method for the given property.
     * 
     * @param m The property read method
     * @return The method for the given property
     */
    private static MethodNode addProperty(Method m) {
        // no exception declaration - we don't throw them :)
        final MethodNode mn = new MethodNode(ACC_PUBLIC | ACC_FINAL, m.getName(), Type.getMethodDescriptor(m), null, null);
        final InsnList inst = mn.instructions;
        inst.add(new LdcInsnNode(mn.name));
        inst.add(new MethodInsnNode(INVOKESTATIC, EL_RECORDER_TYPE, "append", "(Ljava/lang/String;)V"));
        mn.maxLocals = 1;
        mn.maxStack = 1;
        final Class<?> type = m.getReturnType();
        // is the return value a primitive type?
        if (type.isPrimitive())
            return appendPrimitiveReturn(mn, type);
        // we can't subclass arrays or final classes ...
        if (type.isArray() || Modifier.isFinal(type.getModifiers())) {
            inst.add(new InsnNode(ACONST_NULL));
        } else {
            // normal class - return an other recorder instance...
            final Type intern = Type.getType(type);
            inst.add(new LdcInsnNode(intern));
            inst.add(new MethodInsnNode(INVOKESTATIC, PROPERTY_RESOLVER, "get0", "(Ljava/lang/Class;)Ljava/lang/Object;"));
            inst.add(new TypeInsnNode(CHECKCAST, intern.getInternalName()));
        }
        inst.add(new InsnNode(ARETURN));
        return mn;
    }

    /**
     * Appends the return instructions to the passed method node and sets {@link MethodNode#maxStack} to the correct value.
     * 
     * @param mn The method node
     * @param type The primitive return type
     * @return The method node
     */
    private static MethodNode appendPrimitiveReturn(MethodNode mn, Class<?> type) {
        final InsnList inst = mn.instructions;
        if (int.class == type || boolean.class == type || char.class == type || byte.class == type || short.class == type) {
            inst.add(new InsnNode(ICONST_0));
            inst.add(new InsnNode(IRETURN));
            mn.maxStack = 1;
            return mn;
        }
        if (long.class == type) {
            inst.add(new InsnNode(LCONST_0));
            inst.add(new InsnNode(LRETURN));
            mn.maxStack = 2;
            return mn;
        }
        if (float.class == type) {
            inst.add(new InsnNode(FCONST_0));
            inst.add(new InsnNode(FRETURN));
            mn.maxStack = 1;
            return mn;
        }
        if (double.class == type) {
            inst.add(new InsnNode(DCONST_0));
            inst.add(new InsnNode(DRETURN));
            mn.maxStack = 2;
            return mn;
        }
        throw new IllegalArgumentException(type.getName() + " is not a primitive type!");
    }

    /**
     * Creates a suitable {@link InstantiationProvider}.
     * 
     * @return The provider
     */
    private static InstantiationProvider createProvider() {
        // check if the Sun reflection provider is available
        if (System.getProperty("java.vm.vendor").contains("Sun"))
            try {
                Class.forName("sun.reflect.ReflectionFactory");
                return new SunVersion();
            } catch (ClassNotFoundException e) {
            }
        return new Serialization();
    }

    /**
     * Provider for object instantiation.
     * 
     * @author M. Hautle
     */
    private interface InstantiationProvider {
        /**
         * Creates a new instance for the given class.
         * 
         * @param c The class
         * @return A new instance
         */
        Object newInstance(Class<?> c);

        /**
         * Adapts the class for this instantiation provider.
         * 
         * @param cn The class
         */
        void prepareForInstantiation(ClassNode cn);
    }

    /**
     * {@link InstantiationProvider} creating the instance by 'deserializing' it.
     * 
     * @author M. Hautle
     */
    private static class Serialization implements InstantiationProvider {
        /**
         * {@inheritDoc}
         */
        @Override
        public Object newInstance(final Class<?> c) {
            try {
                final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(getSerialized(c.getName()))) {
                    @Override
                    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
                        return c;
                    }
                };
                final Object o = in.readObject();
                in.close();
                return o;
            } catch (Exception e) {
                throw new TechnicalException("Error while trying to deserialize " + c.getName(), e);
            }
        }

        /**
         * Returns the bytes representing an instance in a serialized stream.
         * 
         * @param name The class name
         * @return The serialized bytes
         * @throws IOException If something went wrong
         */
        private byte[] getSerialized(String name) throws IOException {
            final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            final DataOutputStream stream = new DataOutputStream(bytes);
            stream.writeShort(ObjectStreamConstants.STREAM_MAGIC);
            stream.writeShort(ObjectStreamConstants.STREAM_VERSION);
            stream.writeByte(ObjectStreamConstants.TC_OBJECT);
            stream.writeByte(ObjectStreamConstants.TC_CLASSDESC);
            stream.writeUTF(name);
            stream.writeLong(0l);
            // define as serializable
            stream.writeByte(2);
            // no fields
            stream.writeShort(0);
            stream.writeByte(ObjectStreamConstants.TC_ENDBLOCKDATA);
            stream.writeByte(ObjectStreamConstants.TC_NULL);
            stream.close();
            return bytes.toByteArray();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        @SuppressWarnings("unchecked")
        public void prepareForInstantiation(ClassNode cn) {
            cn.interfaces.add(SERIALIZABLE);
            cn.fields.add(new FieldNode(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "serialVersionUID", "J", null, new Long(0L)));
        }
    }

    /**
     * {@link InstantiationProvider} using Sun internals.
     * 
     * @author M. Hautle
     */
    private static class SunVersion implements InstantiationProvider {
        /** The internal reflection factory. */
        private final ReflectionFactory factory = ReflectionFactory.getReflectionFactory();

        /** Constructor of {@link Object}. */
        private final Constructor<Object> CONS;

        /**
         * Default constructor.
         */
        SunVersion() {
            try {
                CONS = Object.class.getConstructor(new Class[0]);
            } catch (Exception e) {
                throw new TechnicalException("Error while looking up the default constructor", e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Object newInstance(Class<?> c) {
            try {
                return factory.newConstructorForSerialization(c, CONS).newInstance();
            } catch (Exception e) {
                throw new TechnicalException("Error while trying to instanciate " + c.getName(), e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void prepareForInstantiation(ClassNode cn) {
        }
    }
}
